package network_programming.np2_nonblocking_communication;

import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.InetAddress;
import java.net.InetSocketAddress;
import java.net.UnknownHostException;
import java.nio.channels.AsynchronousSocketChannel;
import java.util.LinkedList;
import java.util.concurrent.*;

/**
 * 异步通道
 * 使用异步通道，可以使程序并行执行多个异步操作
 *
 * AsynchronousSocketChannel
 * 从JDK7开始引入该类
 * 该类的作用与SocketChannel类相似
 * 区别是：异步通道的一些方法总是采用非阻塞模式，且异步通道的非阻方法返回一个Future对象，用于存放异步运算结果
 *
 * 非阻塞方法：
 *      Future<Void> connect(SocketAddress remote)      连接远程主机
 *      Future<Integer> read(ByteBuffer dst)            从通道中读入数据，存放到ByteBuffer中，
 *                                                          返回的Future对象中包含了实际从通道中读到的字节数
 *      Future<Integer> writer(ByteBuffer src)          把ByteBuffer中的数据写入通道中，
 *                                                          返回的Future对象包含了实际写入的字节数
 *
 *
 * Void类
 * 是一个不可实例化的占位符类，如果方法返回值是Void类型，那么该方法只能返回null类型
 *
 *
 * 本例演示异步通道的用法
 * 不断接受用户输入的域名（即网络上的主机名），然后与这个主机上的80端口（即HTTP服务）建立连接，最后打印建立连接所消耗的时间。
 * 如果程序无法连接到指定的主机，就打印相关错误信息。如果用户输入“bye”则结束程序。
 * */
public class AsynchronousSocketChannelDemo {
    private final LinkedList<PingResult> pingResults = new LinkedList<>();
    boolean shutdown = false;
    ExecutorService executorService;

    public AsynchronousSocketChannelDemo() {
        executorService = Executors.newFixedThreadPool(4);
        executorService.execute(new Printer());
        receivePingAddress();
    }

    public static void main(String[] args) {
        new AsynchronousSocketChannelDemo();
    }

    /** 接收用户输入的主机地址，由线城市执行PingHandler任务 */
    public void receivePingAddress() {
        BufferedReader localReader = new BufferedReader(new InputStreamReader(System.in));
        String msg = null;
        while (true) {
            try {
                if (((msg = localReader.readLine()) != null)) {
                    if (msg.equals("bye")) {
                        shutdown = true;
                        executorService.shutdown();
                        break;
                    }
                    executorService.execute(new PingHandler(msg));
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
        }
    }

    /** 尝试连接特定主机，并把运算结果加入PingResults结果队列中 */
    public void addPingResult(PingResult pingResult) {
        AsynchronousSocketChannel socketChannel = null;
        try {
            socketChannel = AsynchronousSocketChannel.open();
            pingResult.socketChannel = socketChannel;
            pingResult.connectStart = System.currentTimeMillis();
            synchronized (pingResults) {
                pingResults.add(pingResult);
                pingResults.notify();       // 唤醒在该对象上阻塞的线程
            }
            Future<Void> connectResult = socketChannel.connect(pingResult.address);
            pingResult.connectResult = connectResult;
        } catch (IOException e) {
            if (socketChannel != null) {
                try {
                    socketChannel.close();
                } catch (IOException ioException) {
                    ioException.printStackTrace();
                }
            }
            pingResult.failure = pingResult.ERROR;
        }
    }

    /** 打印PingResults结果队列中已经执行完毕的任务结果 */
    public void printPingResults() {
        PingResult pingResult = null;
        while (!shutdown) {
            synchronized (pingResults) {
                while (!shutdown && pingResults.size() == 0) {
                    try {
                        pingResults.wait(100);
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
                if (shutdown && pingResults.size() == 0)
                    break;
                pingResult = pingResults.getFirst();
                if (pingResult.connectResult != null) {
                    try {
                        pingResult.connectResult.get(500, TimeUnit.MILLISECONDS);
                    } catch (InterruptedException | ExecutionException | TimeoutException e) {
                        pingResult.failure = pingResult.ERROR;
                    }
                    if (pingResult.connectResult.isDone())
                        pingResult.connectFinish = System.currentTimeMillis();
                    if (pingResult.connectResult.isDone() || pingResult.failure != null) {
                        pingResult.print();
                        pingResults.removeFirst();
                        try {
                            pingResult.socketChannel.close();
                        } catch (IOException e) { }
                    }
                }
            }
        }
    }

    /** 尝试连接特定主机，生成一个PingResult对象，并把它加入PingResults队列中 */
    public class PingHandler implements Runnable {
        String msg;

        public PingHandler(String msg) {
            this.msg = msg;
        }

        @Override
        public void run() {
            if (!"bye".equals(msg)) {
                PingResult pingResult = new PingResult(msg);
                addPingResult(pingResult);
            }
        }
    }

    /** 打印PingResults结果队列中已经执行完毕的任务结果 */
    private class Printer implements Runnable {

        @Override
        public void run() {
            printPingResults();
        }
    }
}


class PingResult {  // 表示连接一个主机的结果
    InetSocketAddress address;
    long connectStart;          // 开始连接的时间
    long connectFinish = 0;     // 连接成功的时间
    String failure;
    Future<Void> connectResult; // 连接操作的异步运算结果
    AsynchronousSocketChannel socketChannel;
    String host;
    final String ERROR = "连接失败";

    PingResult(String host) {
        this.host = host;
        try {
            address = new InetSocketAddress(InetAddress.getByName(host), 80);
        } catch (UnknownHostException e) {
            failure = ERROR;
        }
    }

    public void print() {   // 打印连接一个主机的执行结果
        String result;
        if (connectFinish != 0)
            result = (connectFinish - connectStart) + "ms";
        else if (failure != null)
            result = failure;
        else
            result = "Timed out";
        System.out.println("ping " + host + "的结果：" +result);
    }
}